import { Component } from 'react';

interface IProps {
  onOpen?: (obj: any, instance: Websocket) => void;
  onClose?: (instance: Websocket) => void;
  socketReceiveMsg: (data: any) => void;
  url?: string;
}

const reconnectTimeLimit = 1000; // 默认重连尝试间隔

export default class Websocket extends Component<IProps> {
  url = this.getUrl();
  awaitData = null;

  public getUrl() {
    return `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${this.props.url ?? import.meta.env.websocketUrl
      }?token=${localStorage.getItem('token') || ''}&group=3&client=web`;
  }

  ws = new WebSocket(this.url);

  heartCheck = {
    timeout: 1000 * 25, // 25秒发一次心跳d z
    serverTimeout: 1000 * 2, // 多久内未收到消息自动重连
    reconnectTimeout: reconnectTimeLimit, // 重连尝试间隔
    timeoutObj: null as any,
    serverTimeoutObj: null as any,
    reconnectTimeoutObj: null as any,
    reset() {
      clearTimeout(this.timeoutObj as any);
      clearTimeout(this.serverTimeoutObj as any);
      this.reconnectTimeout = reconnectTimeLimit;
      return this;
    },
    start: () => {
      this.heartCheck.timeoutObj = setTimeout(() => {
        try {
          // 这里发送一个心跳，后端收到后，返回一个心跳消息，
          // onmessage拿到返回的心跳就说明连接正常
          this.ws.send('ping');
          this.heartCheck.serverTimeoutObj = setTimeout(() => {
            // 如果超过一定时间还没重置，说明后端主动断开了
            this.ws.close(); // close后会延迟数十秒触发onClose, 必须手动重连
            this.reconnect();
          }, this.heartCheck.serverTimeout);
        } catch (error) {
          // console.log(error, 'error');
        }
      }, this.heartCheck.timeout);
    },
  };

  componentDidMount() {
    this.assignWs();
  }

  public componentWillUnmount() {
    if (this.ws) {
      this.ws.close();
      this.ws.onmessage = null;
      this.ws.onclose = null;
      this.ws.onerror = null;
      this.ws.onopen = null;
      this.ws = null as any;
      this.heartCheck.reset();
    }
  }

  public send(data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(data);
      this.heartCheck.reset().start(); // 心跳检测重置
    } else {
      console.log('ws连接已断开 尝试重连中');
      this.awaitData = data
      this.reconnect(this.getRandomTimeOffset())
    }
  }

  public close() {
    if (this.ws) {
      this.ws.close();
    }
  }

  public assignWs = () => {
    this.ws.onclose = this.onClose;

    this.ws.onerror = this.onError;

    this.ws.onopen = this.onOpen;

    this.ws.onmessage = this.onMessage;
  };

  public createWebSocket = () => {
    try {
      this.ws = new WebSocket(this.getUrl());
      this.assignWs();
    } catch (e) {
      this.reconnect();
      // console.log(e, 'error');
    }
  };

  public onMessage = (e: any) => {
    if (e.data === 'pong') this.heartCheck.reset().start();
    this.props.socketReceiveMsg?.(e);
  };

  public onOpen = (e: any) => {
    this.props?.onOpen?.(e, this);
    console.log(
      '%cws open',
      'color: #0f0;background-color: #000;padding: 0 4px',
    );
    this.heartCheck.reset().start(); // 心跳检测重置
    if (this.awaitData) {
      this.send(this.awaitData)
      this.awaitData = null
    }
  };

  public onClose = () => {
    this.props?.onClose?.(this);
    console.log(
      '%cws close',
      'color: #888;background-color: #000;padding: 0 4px',
    );
    this.reconnect();
  };

  public onError = () => {
    console.log(
      '%cws error',
      'color: #f00;background-color: #000;padding: 0 4px',
    );
    this.reconnect();
  };

  public reconnect = (timeOffset?: number) => {
    if (!this.ws || !localStorage.getItem('token')) return;
    clearTimeout(this.heartCheck.reconnectTimeoutObj);
    this.heartCheck.reconnectTimeoutObj = setTimeout(() => {
      this.ws = null as any;
      // 没连接上会一直重连, 重连时间间隔每次会翻倍, 300s封顶
      this.createWebSocket();
      this.heartCheck.reconnectTimeout = Math.min(
        this.heartCheck.reconnectTimeout * 2,
        300 * 1000,
      );
    }, this.heartCheck.reconnectTimeout + (timeOffset ?? 0));
  };

  private getRandomTimeOffset(): number {
    return Math.floor(Math.random() * 2000)
  }

  render() {
    return <span />;
  }
}
